/*
 * Copyright 2006-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.martinimix.dao.account;

import java.util.List;

import net.martinimix.beans.bind.BindingService;
import net.martinimix.beans.bind.BindingServiceAware;
import net.martinimix.dao.DataAccessException;
import net.martinimix.dao.SimpleBusinessActionTemplate;
import net.martinimix.dao.environment.EnvironmentDao;
import net.martinimix.domain.account.Address;
import net.martinimix.domain.account.Customer;
import net.martinimix.domain.account.InvalidLoginException;
import net.martinimix.domain.payment.Account;
import net.martinimix.domain.payment.CreditCard;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.bluemartini.database.BMDatabaseManager;
import com.bluemartini.database.BindArray;
import com.bluemartini.database.DBUtil;
import com.bluemartini.dna.BMContext;
import com.bluemartini.dna.BMException;
import com.bluemartini.dna.BMThreadManager;
import com.bluemartini.dna.BusinessObject;
import com.bluemartini.dna.BusinessObjectArray;
import com.bluemartini.dna.DNAList;
import com.bluemartini.html.HTMLContext;
import com.bluemartini.htmlapp.AddressArray;
import com.bluemartini.htmlapp.HTMLUserAccountUtil;
import com.bluemartini.htmlapp.UserAccount;
import com.bluemartini.server.BMClient;
import com.bluemartini.util.AttributeUtil;
import com.bluemartini.util.UserAccountUtil;
import com.bluemartini.webconnect.WebConnect;

/**
 * Provides a Blue Martini implementation of an account data access object.
 * 
 * @author Scott Rossillo
 *
 */
public class BlueMartiniAccountDao extends SimpleBusinessActionTemplate 
		implements BindingServiceAware, AccountDao {
	
	private final static String CREDIT_CARD_BIZOBJ_NAME = "CREDIT_CARD";
	private final static String USER_ACCOUNT_BIZOBJ_NAME = "USER_ACCOUNT";
	
	private final static String SELECT_USER_ID_SQL = "SELECT USA_ID FROM USER_ACCOUNT WHERE USA_UID = ?";
	
	private static final Log log = LogFactory.getLog(BlueMartiniAccountDao.class);
	
	private EnvironmentDao environmentDao;
	private String selectCreditCardsBusinessAction = "GetCreditCardsForUser";
	
	private PrototypeAccountFactory prototypeAccountFactory;
	
	/**
	 * Constructs a new Blue Martini account data access object.
	 */
	public BlueMartiniAccountDao() {
		super();
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.bind.BindingServiceAware#setBindingService(net.martinimix.dao.bind.BindingService)
	 */
	public void setBindingService(BindingService bindingService) {
		
		super.bindingService = bindingService;
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#createGuestCustomer()
	 */
	public Customer createGuestCustomer() {
		
		DNAList input = new DNAList();
		BusinessObject boUserAccount = BMContext.createBusinessObject("USER_ACCOUNT");
		
		boUserAccount.setLong("createdBy", environmentDao.getDefaultUserId());
		boUserAccount.setLong("modifiedBy", environmentDao.getDefaultUserId());
		boUserAccount.setInteger("loginCount", 1);
		
		input.setBusinessObjectRef("USER_ACCOUNT", boUserAccount);
		
		return (Customer) super.execute(
				"CreateCustomer", prototypeAccountFactory.createCustomerBean(), "USER_ACCOUNT", input);
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#findAddress(long)
	 */
	public Address findAddress(long id) {
		
		BusinessObject boAddress = null;
		
		try {
			BMDatabaseManager.pushCurrentDatabase("store");
			boAddress = HTMLUserAccountUtil.getAddressDetail(new Long(id));
		} catch (BMException e) {
			throw new DataAccessException("Error fetching address " + id, e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");	
		}
		
		return (Address) bindingService.bindBean(boAddress, new Address());
	}
	
	
	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#findCustomer(long)
	 */
	public Customer findCustomer(long customerId) {
		
		BusinessObject boUser = null;
		Customer customer;
		
		try {
			BMDatabaseManager.pushCurrentDatabase("store");
			boUser = HTMLUserAccountUtil.getUserAccountDetail(new Long(customerId));
		} catch (BMException e) {
			throw new DataAccessException("Error fetching customer [" + customerId + "]", e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");	
		}
		
		customer = (Customer) bindingService.bindBean(boUser, prototypeAccountFactory.createCustomerBean());
		
		return customer;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#findCustomer(java.lang.String)
	 */
	public Customer findCustomer(String username) {
		
		BindArray ba = new BindArray();
		long id;
		
		if(username != null) {
			username = username.toLowerCase();
		}
		
		BMDatabaseManager.pushCurrentDatabase("store");
		try {
			ba.addString(username);
			id = DBUtil.selectLong(SELECT_USER_ID_SQL, ba);
		} catch (BMException e) {
			throw new DataAccessException("Error fetching customer [" + username + "]", e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");	
		}
		
		return id > 0 ? this.findCustomer(id) : null;
	}
	
	
	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#getBillingAddresses(long)
	 */
	public List getBillingAddresses(long customerId) {
		
		AddressArray addressArray = getCustomerAddresses(customerId);
		BusinessObjectArray shippingAddressArray = this.parseAddresses(addressArray, Address.TYPE_BILLING);
		
		return this.bindingService.bindBeans(shippingAddressArray, new Address());
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#getCustomerShippingAddresses(long)
	 */
	public List getShippingAddresses(long customerId) {
		
		AddressArray addressArray = getCustomerAddresses(customerId);
		BusinessObjectArray shippingAddressArray = this.parseAddresses(addressArray, Address.TYPE_SHIPPING);
		
		return this.bindingService.bindBeans(shippingAddressArray, new Address());
	}
	
	/**
	 * Returns an address array containing all known addresses for the given customer.
	 * 
	 * @param customerId the id for the customer whose addresses
	 * should be returned
	 * 
	 * @return an <code>AddressArray</code> containing all addresses
	 * for the given <code>customerId</code>
	 */
	protected final AddressArray getCustomerAddresses(long customerId) {
		
		BusinessObject boUserAccount = BMContext.createBusinessObject("USER_ACCOUNT");
		AddressArray addressArray;
		boUserAccount.setLong("usa_id", customerId);
		UserAccount act = new UserAccount(boUserAccount);
		
		BMDatabaseManager.pushCurrentDatabase("store");
		try {
			addressArray = HTMLUserAccountUtil.getAddresses(act);
		} catch (BMException e) {	
			throw new DataAccessException("Unable to get addresses for customer [" + customerId + "]!", e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");
		}
		
		return addressArray;
	}
	
	/**
	 * Returns the password for the customer identified by the given customer id.
	 * 
	 * @param customerId the identifier for the <code>Customer</code> whose
	 * password should be returned
	 * 
	 * @return the password for the customer identified by the given <code>customerId</code>
	 */
	protected final String getCustomerPassword(Long customerId) {
		
		String password;
		
		BMDatabaseManager.pushCurrentDatabase("store");
		try {
			password = UserAccountUtil.getUserAccountPassword(customerId);
		} catch (BMException e) {
			throw new DataAccessException(
					"Unable to get password for customer [" + customerId + "]", e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");
		}
		
		return password;
	}
	
	private BusinessObjectArray parseAddresses(AddressArray addressArray, String addressType) {
		
		BusinessObjectArray boAddressArray = new BusinessObjectArray();
		com.bluemartini.htmlapp.Address address;
		
		for(int i = 0; i < addressArray.size(); i++) {
			address = addressArray.getAddress(i);
			if(address.getString("type_cd", "").equalsIgnoreCase(addressType) ) {
				boAddressArray.addElement(address);
			}
		}
		
		return boAddressArray;
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#login(java.lang.String, java.lang.String)
	 */
	public Customer login(String userId, String password) throws InvalidLoginException {
		
		DNAList dnaIn = new DNAList();
		DNAList formData = new DNAList();
		DNAList result;
		
		if(log.isDebugEnabled()) {
			log.debug("Logging in user [" + userId + "] with password [" + password + "]");
		}
		
		if(userId == null || password == null) {
			throw new IllegalArgumentException("Username and password must not be null!");
		}
		
		BusinessObject login = BMContext.createBusinessObject(WebConnect.SESSION_LOGIN_USER);
		BusinessObject boAccount;
		BusinessObject boLogin;
		login.setString("userid", userId);
		login.setString("password", password);
		
		log.debug("userID: " + userId);
		log.debug("userID: " + password);
		
		formData.setBusinessObjectRef(WebConnect.SESSION_LOGIN_USER, login);
		dnaIn.setListRef(HTMLContext.REQUEST_FORM_DATA, formData);
		
		try {
			result = BMClient.executeBusinessAction("WebConnectLogin", dnaIn);
			boAccount = result.removeBusinessObject("CURRENT_USER");
			boLogin = result.getBusinessObject("LOGIN");
			
			if(log.isDebugEnabled()) {
				log.debug("Login result: " + result);
				log.debug("boAccount: " + boAccount);
				log.debug("boLogin: " + boLogin);
			}
			
			if(boAccount == null) {
				int intFailure = -1;
				intFailure = boLogin == null ? -1 : boLogin.getInteger("failureReason", -1);
				if(intFailure == 1){
					throw new InvalidLoginException("LOGIN_FAILED_LOCKEDOUT");
				} else {
					throw new InvalidLoginException("INVALID_LOGIN");
				}
			} 
			
			BMDatabaseManager.pushCurrentDatabase("store");
			try {
				boAccount = HTMLUserAccountUtil.getUserAccountDetail(boAccount.getLongID());
				if(log.isDebugEnabled()) {
					log.debug("USER ACCONT: " + boAccount);
				}
			} finally {
				BMDatabaseManager.popCurrentDatabaseQuiet("store");
			}
			
		} catch (BMException e) {
			throw new DataAccessException("Error logging in user", e);
		}
		
		return (Customer) bindingService.bindBean(boAccount, prototypeAccountFactory.createCustomerBean());
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#saveAddresss(net.martinimix.domain.account.Address)
	 */
	public Address saveAddresss(Address address) {
		
		BusinessObject boAddress = bindingService.bindBusinessObject(address, "ADDRESS");
		
		boAddress.setLong("createdBy", address.getCustomerId());
		boAddress.setLong("modifiedBy", address.getCustomerId());
		
		try {
			BMDatabaseManager.pushCurrentDatabase("store");
			if(address.getId() == null) {
				DBUtil.insertBusinessObject(boAddress, true);
				address.setId(boAddress.getLongID());
			} else {
				DBUtil.updateBusinessObject(boAddress);
			}
		} catch (BMException e) {
			throw new DataAccessException("Unable to save address!", e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");
		}
		
		return (Address) bindingService.bindBean(boAddress, address);
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#saveCustomer(net.martinimix.domain.account.Customer)
	 */
	public void saveCustomer(Customer customer) {
		
		DNAList input = new DNAList();
		BusinessObject boUserAccount = bindingService.bindBusinessObject(customer, "USER_ACCOUNT");
		
		Long currentUser = BMThreadManager.getUserAccountID();
		
		if(currentUser == null) {
			currentUser = environmentDao.getDefaultUserId();
		}
		
		if(customer.getId() == null) {
			boUserAccount.setLong("createdBy", currentUser);
			boUserAccount.setLong("modifiedBy", currentUser);
			boUserAccount.setInteger("loginCount", 1);
		} else {
			boUserAccount.setLong("modifiedBy", currentUser);
			
			// BM resets the password to null if it's missing.
			// Registered users should never have a null password.
			// This is the work-around ...
			if(customer.getPassword() == null) {
				log.warn("Customer [" + customer.getId() + "]'s password protection required.");
				boUserAccount.setString("password", this.getCustomerPassword(customer.getId()));
			}
		}
		
		input.setBusinessObjectRef("USER_ACCOUNT", boUserAccount);
		
		if(log.isDebugEnabled()) {
			log.debug("Saving user account: " + boUserAccount);
		}
		
		super.execute("UpdateUserAccount", prototypeAccountFactory.createCustomerBean(), "USER_ACCOUNT", input);
		
		if(customer.getId() != null) {
			// UpdateUserAccount bizact fails to store object attributes
			BMDatabaseManager.pushCurrentDatabase("store");
			try {
				AttributeUtil.storeObjectAttributes(boUserAccount);
			} catch (BMException e) {
				throw new DataAccessException("Error storing user account [" + customer.getId() + "] attributes!", e);
			} finally {
				BMDatabaseManager.popCurrentDatabaseQuiet("store");
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.dao.payment.PaymentDao#getCreditCard(long)
	 */
	public CreditCard getCreditCard(long creditCardId) {
		
		BusinessObject boCreditCard;
		
		BMDatabaseManager.pushCurrentDatabase("store");
		try {
			boCreditCard = HTMLUserAccountUtil.getCreditCard(new Long(creditCardId));
		} catch (BMException e) {
			throw new DataAccessException("Unable to get credit card with id [" + creditCardId + "]");
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");
		}
		
		return (CreditCard) bindingService.bindBean(boCreditCard, new CreditCard());
	}

	/* (non-Javadoc)
	 * @see net.martinimix.dao.payment.PaymentDao#getCreditCards(long)
	 */
	public List getCreditCards(long customerId) {
	
		DNAList input = new DNAList();
		DNAList response;
		
		input.setLong("usa_id", customerId);
		try {
			response = BMClient.executeBusinessAction(selectCreditCardsBusinessAction, input);
			
			if(log.isDebugEnabled()) {
				log.debug("Customer [" + customerId + "] get credit cards response: " + response);
			}
			
		} catch (BMException e) {
			throw new DataAccessException("Unable to retrieve credit cards for customer [" + customerId + "]");
		}
		
		return bindingService.bindBeans(response.removeBusinessObjectArray("CREDIT_CARD_ARRAY"), new CreditCard());
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.dao.account.AccountDao#saveAccount(net.martinimix.domain.payment.Account, net.martinimix.domain.account.Customer)
	 */
	public void saveAccount(Account account, Customer customer) {
		
		BusinessObject boAccount = bindingService.bindBusinessObject(account, "ACCOUNT");
		//BusinessObject boUserAccount = bindingService.bindBusinessObject(customer, USER_ACCOUNT_BIZOBJ_NAME);
		
		if(account.getId() == null) {
			boAccount.setLong("createdBy", customer.getId());
		}
		boAccount.setLong("modifiedBy", customer.getId());
		
		if(account.getCurrencyCode() == null) {
			account.setCurrencyCode("USD");
			boAccount.setString("currency_cd", "USD");
		}
		
		if(account.getStatusCode() == null) {
			account.setStatusCode(Account.STATUS_ACTIVE);
			boAccount.setString("status_cd", Account.STATUS_ACTIVE);
		}
		
		if(log.isDebugEnabled()) {
			log.debug("Ready to save account " + boAccount);
		}

		BMDatabaseManager.pushCurrentDatabase("store");
		try {
			if(account.getId() == null) {
				DBUtil.insertBusinessObject(boAccount, true);
			} else {
				DBUtil.insertBusinessObject(boAccount, true);
			}
			bindingService.bindBean(boAccount, account);
		} catch (BMException e) {
			if(log.isDebugEnabled()) {
				log.debug("Failed to save [Account]", e);
			}
			throw new DataAccessException("Unable to save [Account]!", e);
		} finally {
			BMDatabaseManager.popCurrentDatabaseQuiet("store");
		}
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.dao.payment.PaymentDao#saveCreditCard(net.martinimix.domain.account.Customer, com.bluemartini.htmlapp.CreditCard)
	 */
	public void saveCreditCard(Customer customer, CreditCard creditCard) {
		
		BusinessObject boCreditCard = bindingService.bindBusinessObject(creditCard, CREDIT_CARD_BIZOBJ_NAME);
		BusinessObject boUserAccount = bindingService.bindBusinessObject(customer, USER_ACCOUNT_BIZOBJ_NAME);
		DNAList input = new DNAList();
		DNAList response;
		
		if(creditCard.getId() == null) {
			boCreditCard.setLong("createdBy", customer.getId());
		}
		boCreditCard.setLong("modifiedBy", customer.getId());
		
		input.setBusinessObjectRef(USER_ACCOUNT_BIZOBJ_NAME, boUserAccount);
		input.setBusinessObjectRef(CREDIT_CARD_BIZOBJ_NAME, boCreditCard);
		
		if(log.isDebugEnabled()) {
			log.debug("Saving credit card with dna: " + input);
		}
		
		try {
			response = HTMLUserAccountUtil.updateCreditCard(input);
			bindingService.bindBean(response.removeBusinessObject(USER_ACCOUNT_BIZOBJ_NAME), customer);
			bindingService.bindBean(response.removeBusinessObject(CREDIT_CARD_BIZOBJ_NAME), creditCard);
		} catch (BMException e) {
			throw new DataAccessException("Unable to save [CreditCard]!", e);
		}
	}


	/**
	 * Sets the environment data access object for this account dao.
	 * 
	 * @param environmentDao
	 */
	public void setEnvironmentDao(EnvironmentDao environmentDao) {
		this.environmentDao = environmentDao;
	}
	
	public void setSelectCreditCardsBusinessAction(
			String selectCreditCardsBusinessAction) {
		this.selectCreditCardsBusinessAction = selectCreditCardsBusinessAction;
	}

	/**
	 * Sets the prototype account factory for this account data acccess object.
	 * 
	 * @param prototypeAccountFactory the <code>PrototypeAccountFactory</code>
	 * for this dao
	 */
	public void setPrototypeAccountFactory(
			PrototypeAccountFactory prototypeAccountFactory) {
		this.prototypeAccountFactory = prototypeAccountFactory;
	}
}
